package org.NooLab.compare.utilities.math;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import org.apache.commons.collections.CollectionUtils;
// import org.apache.commons.lang3.ArrayUtils;

import org.NooLab.compare.CompareGlobals;


/*
 * resources:  http://en.wikipedia.org/wiki/Bregman_divergence
 */


/**
 * 
 * 
 */
public class Distance implements DistanceIntf{


	DistanceControlProperties ctrlProperties ;
	
	/** nothing will happen if this has not been set, values are: (int)plain, vector, text ....
	 *  as inherited from DataDomainGlobals 
	 */
	int dataDomainMarker = -1 ;
	
	transient private int additionalMVcount=0;
	
	/** this distance method i sonly for numerical vectors */
	int distanceMethod = DistanceControlProperties.__distanceSPELA ;
	
	Vector<Double> vector1 = new Vector<Double>() ; 
	Vector<Double> vector2 = new Vector<Double>() ;
	
	Vector<String> items1 = new Vector<String>() ; 
	Vector<String> items2 = new Vector<String>() ;

	
	
	public Distance( DistanceControlProperties cprops){
		ctrlProperties = cprops;
	}

	public Distance(){
		
		ctrlProperties = new DistanceControlProperties();
		distanceMethod = ctrlProperties.getDistanceMethod();
	}
	
	
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Distance setData( double[] vector_1, double[] vector_2 ){
		int p,q,k ;
		 
		// apache: ArrayUtils.
		
		vector1 = new Vector( asList(vector_1) ) ;
		vector2 = new Vector( asList(vector_2) ) ;

		// standard method from "Arrays" will work only for String..., 
		// for any other type it creates a list, of which the whole array is the the first item... 
		// vector2 = new Vector(Arrays.asList(vector_2)) ; 

		if (vector1.size() !=  vector2.size()){
			// __MV_ByHardExcludLengthAdj ? remove positions that are too much
			p = vector1.size() ;
			q = vector2.size() ;
			k = Math.min(p, q) ;
			if (vector1.size()>k){
				// put all of the rest to -1.0
				for (int i=k;i<vector1.size();i++){
					vector1.set(i,  CompareGlobals.__MV) ;
				}
			}
			if (vector2.size()>k){
				// put all of the rest to -1.0
				for (int i=k;i<vector2.size();i++){
					vector2.set(i,  CompareGlobals.__MV) ;
				}
			}
			vector1.setSize(p) ;
			vector2.setSize(p) ;
			additionalMVcount = Math.max(vector1.size(), vector2.size())-p ;
		}
		
		return this;
	}

	/*    
	 	public List<Integer> asList(final int[] is){
            return new AbstractList<Integer>() {
                    public Integer get(int i) { return is[i]; }
                    public int size() { return is.length; }
            };
    }

	 
	 */
    private List<Double> asList(final double[] is){
    	AbstractList<Double> lD;
    	
        lD = new AbstractList<Double>() {
                    public Double get(int i) { return is[i]; }
                    public int size() { return is.length; }
            };
            
            return lD;
    }

    
	@SuppressWarnings("unchecked")
	public Distance setData( Vector<Double> vector_1, Vector<Double> vector_2 ){
	
		vector1 = (Vector<Double>) vector_1.clone() ; // only working for string conversions ?
		vector2 = (Vector<Double>) vector_2.clone() ;
		 
		return this;
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Distance setData( String[] items_1, String[] items_2 ){
		 
		items1 = new Vector(Arrays.asList(items_1)) ;
		items2 = new Vector(Arrays.asList(items_2)) ; 
		 
		return this;
	}

	public Distance setDomain( int datadomain){
		
		dataDomainMarker = datadomain;
		return this;
	}
	
	/**
	 * 
	 * 
	 * @param featuresfields
	 * @return
	 */
	public Distance setReferenceFeatureVector( String[] featuresfields ){
		
		return this;
	}
	
	public Distance setComparedFeatureVector(String[] featuresfields ){
		
		
		return this;
	}
	
	
	// ------------------------------------------------------------------------
	public double calculate(){
	
		double result = CompareGlobals.__MV ;
		
		try{
			
			distanceMethod = ctrlProperties.getSetSimMethod() ;
			result = calculate( distanceMethod );
			
		}catch(Exception e){
			e.printStackTrace();
			result = CompareGlobals.__FAILED;
		}finally{
			
		}
		return result;
	}
	
	/**
	 * 
	 * architecture: actually, calculate() should loop over a collection of registered classes,
	 * that offer the basic distance calculation. The 
	 * The programmer may register his own distance-primitive for an extension on this low level
	 * 
	 */
	public double calculate( int methodID ) {
		double result = CompareGlobals.__MV ;
		
		try{
			// ctrlProperties.
			if (dataDomainMarker <= DistanceControlProperties._DD_SIMPLESET){ 
				if (methodID <= DistanceControlProperties.__setsimilarityPLAIN){
					result = simpleSetComparison();
				}
				if (methodID == DistanceControlProperties.__setsimilarityTVERSKY ){ 
					result = generalizedTversky();
				}
				if (methodID == DistanceControlProperties.__setsimilarityCRATIO ){
					result = setMatchingCRatio() ;
				}
				if (methodID == DistanceControlProperties.__setsimilarityJACCARD ){
					result = setMatchingJaccard();
				}
				if (methodID >= DistanceControlProperties.__setsimilaritySORENSEN ){
					result = setMatchingSorensen() ;
				}
				 
			}
			if (dataDomainMarker == DistanceControlProperties._DD_VECTOR){
				if (methodID <= DistanceControlProperties.__distanceEUCLID){
					result = euclideanDistance();
				}
				if (methodID >= DistanceControlProperties.__distanceSPELA){
					result = spelaDistance();
				}
			}
			
			if (dataDomainMarker == DistanceControlProperties._DD_CONTEXT){
				
			}
			
			if (dataDomainMarker == DistanceControlProperties._DD_FREETEXT){
				
			}
			
			if (dataDomainMarker == DistanceControlProperties._DD_IMAGE){
				
			}
			
			if (dataDomainMarker == DistanceControlProperties._DD_STRING){
				
			}
			
			
		}catch(Exception e){
			e.printStackTrace();
			result = CompareGlobals.__FAILED;
		}finally{
			
		}
		return result;
		 
	}
	
	private double calculateDerivedResultsForSets( double inRValue, double overlap, Vector<String> vector1, Vector<String> vector2) {
	
		double result = inRValue, gmn;
		
		if (ctrlProperties.getRelativeDistances() ){ 
			result = result/((double)overlap) ;   // 
		}
		
		if (ctrlProperties.getNormalizedDistances() ){
			int n1 = vector1.size();
			int n2 = vector2.size();
			// geometric mean
			gmn = Math.sqrt(n1*n2);
			if (gmn>0){
				result = result/gmn ;
			}
		}
		return result ;
	}

	private double calculateDerivedResultsForVectors( double inRValue, Vector<Double> vector1, Vector<Double> vector2){
	
		double result = inRValue,   maxD;
		
		// given the maximal possible distance given vector1 is the base vector
		if (ctrlProperties.getRelativeDistances() ){ 
			maxD = calculateMaximumOfPossibleDistance(vector1) ; // 1.118
			result = result/maxD ;   // 0.322
		}
		
		if (ctrlProperties.getNormalizedDistances() ){ 
			result = result/vector1.size() ; // 0.107
		}
		return result ;
	}

	/**
	 * classical form;
	 * includes missing value treatment according to settings, and provides derived results if requested
	 * through DistanceControlProperties;
	 * 
	 * dreived result: (1) relative to maximum
	 * 
	 * 
	 * @return
	 */
	protected double euclideanDistance(){
		double result = CompareGlobals.__FAILED ;
		
		int len;
		double sqdsum=0,  d, d1,d2 ,_mv;
		
		try{
			if ((vector1==null) || (vector2==null)){
				return result;
			}
			
			_mv = CompareGlobals.__MV ;
			len = vector1.size();
			
			for (int i=0; i<len;i++){
				
				d1 = ((double)vector1.get(i)) ;
				d2 =  ((double)vector2.get(i)) ;
				
				if ((d1!= _mv) && (d2!= _mv)){
				
					d = d1-d2 ;
					
				}else{
					d = ctrlProperties.getSurrogateDistance();
					if (d<=0){
						d = 0.0 ;
					}
					if (ctrlProperties.getMissingValueHandling() ==  DistanceControlProperties.__MV_ByAdjSurrogateDistance){
						// d = d/vector1.size();
					}
					if ((d1 == _mv) && (d2 == _mv)){
						d = Math.sqrt(2)*d ;
					}
				}
				sqdsum = sqdsum + ( d*d );
				
			} // i->
			
			if (additionalMVcount>0){
				sqdsum = sqdsum + (additionalMVcount * _mv);
			}
			
			result = Math.sqrt( sqdsum );     // 0.361
			
			result = calculateDerivedResultsForVectors( result, vector1,vector2) ;
			 
			
		}catch(Exception e){
			e.printStackTrace();
		 
		}finally{
			
		}
		return result;
	}

	
	/**
	 * 
	 * heuristic mixture of Euclidean distance and a "form aspect", note that the distance is NOT
	 * normalized to length of vectors;
	 * 
	 * if vectors are of different length, a missing value will be returned
	 * 
	 *
	 * @return
	 */
	protected double spelaDistance(){
		double result = CompareGlobals.__FAILED ;
		double c,d,d0,d1,d2, df,ic1 = 0,ic2=0,iq, _mv, sqdsum=0.0 ; 
		int i,z  ;
		
		try{
			
			if (vector1.size() != vector2.size()) {
				return -1.0;
			}

			d = 0;
			z = 0;
			d0 = 0;
			
			_mv = CompareGlobals.__MV ;
			
			for (i = 0; i < vector2.size(); i++) {

				d1 = vector1.get(i) ;
				d2 = vector2.get(i);
				  
				if ((d1!= _mv) && (d2!= _mv)){
					// standard difference per feature
					c = d1-d2 ;
					
				}else{
					// missing values according to settings 
					c = ctrlProperties.getSurrogateDistance();
					if (c<=0){
						c = 0.0 ;
					}
					 
					if ((d1 == _mv) && (d2 == _mv)){
						c = Math.sqrt(2)*c ;
					}
				}
				
				// standard contribution
				d = d + c * c;
				
				
				// --------------------
				// "form" aspect
				if (c > 0.5) {
					c = c + ((Math.sqrt(c - 0.5)) * 0.812);
					if (c > 1) {
						c = 1;
					}
				}
				d0 = d0 + Math.abs(c);
				z = z + 1;

				c = 0;
				if (i < vector2.size() - 1) {
					ic1 = vector1.get(i) - vector1.get(i + 1);
					ic2 = vector2.get(i) - vector2.get(i + 1);
				} else {
					ic1 = vector1.get(i) - vector1.get(0);
					ic2 = vector2.get(i) - vector2.get(0);
				}
				c = Math.abs(ic1 - ic2);

				if (ic2 != 0) {
					iq = ic1 / ic2;
					if (iq < 0) {
						c = c + Math.sqrt(c / 2);
						if (c > 0.5) {
							c = 0.5;
						}
					}
				}

				sqdsum = sqdsum + 0.3 * Math.abs(c);
			} // i->
			
			if (additionalMVcount>0){
				sqdsum = sqdsum + (additionalMVcount * _mv);
			}

			// de-normalizing
			df = (sqdsum/z + 0.2*(d0/z))*z ;

			result = df;
			
			// relativizing, normalizing, if requested
			result = calculateDerivedResultsForVectors( result, vector1,vector2) ;
			
		}catch(Exception e){
			e.printStackTrace();
		 
		}finally{
			
		}
		return result;
	}
 
	 
	/**
	 * 
	 * In statistics, Mahalanobis distance is a distance measure introduced by P. C. Mahalanobis in 1936.
	 * It is based on correlations between variables by which different patterns can be identified and analyzed. 
	 * It gauges similarity of an unknown sample set to a known one. It differs from Euclidean distance 
	 * in that it takes into account the correlations of the data set and is scale-invariant. 
	 * In other words, it is a multivariate effect size.
	 * 
	 * 
	 * @return
	 */
	protected double Mahalanobis(){
		double result = CompareGlobals.__FAILED ;
		
		// see class ...

		
		return result;
	}
	
	/**
	 * source: http://en.wikipedia.org/wiki/Bregman_divergence
	 * 
	 * originally, the IS distance is defined for comparing spectra;
	 * 
	 * B(F)(p,q) = sum(over i) = ( p(i)/q(i) - log(p(i)/q(i)) -1)
	 * generated by the function -Sum(log p(i))
	 * 
	 * 
	 * 
	 * @return
	 */
	protected double ItakuraSaito(){
		double result = CompareGlobals.__FAILED ;
		
		
		return result;
	}
	
	
	/**
	 * 
	 * Hausdorff distance can be used to determine the topological difference between pairs or triplets
	 * of records : 2 (3) pairs are interpreted as a curve in R2 or R3, (Rn)
	 * -> we need versions that use 4 or 6 records 
	 * ... whole tables could be compared to each other, for new possibilities of segmentations
	 *     also SOMs could be compared
	 * 
	 * Die Hausdorff-Metrik, benannt nach dem Mathematiker Felix Hausdorff, misst den 
	 * Abstand δ(A,B) zwischen nichtleeren kompakten Teilmengen A, B eines metrischen Raums E.
	 * 
	 * the Hausdorff distance, or Hausdorff metric, also called Pompeiu–Hausdorff distance,[1] measures how 
	 * far two subsets of a metric space are from each other. It turns the set of non-empty compact subsets 
	 * of a metric space into a metric space in its own right
	 * 
	 * here...
	 * http://en.wikipedia.org/wiki/Hausdorff_distance
	 * it is explained how it relates to computer vision
	 * 
	 * in jts package there is a java implementation
	 * /jts/algorithm/distance/DiscreteHausdorffDistance.java
	 * 
	 * a context is always a set, in order
	 * http://www.cs.cornell.edu/vision/hausdorff/hausmatch.html
	 * 
	 * in Mathlab
	 * http://www.mathworks.com/matlabcentral/fileexchange/27905-hausdorff-distance
	 * 
	 * Gromov-Hausdorff-Metrik
	 * 
	 * @return
	 */
	protected double hausdorffDistance(){
		
		double result = CompareGlobals.__FAILED ;
		
		int len;
		double sqdsum=0;
		
		try{
			if ((vector1==null) || (vector2==null)){
				return result;
			}
			 
			// in its dedicated class
			
			
			result = Math.sqrt( sqdsum );
			
		}catch(Exception e){
			e.printStackTrace();
		 
		}
		return result;
		
	}

	/**
	 * this creates a simple ratio of elements common to both sets divided (normalized)
	 * by the length of the set 1 (reference set) 
	 */
	protected double simpleSetComparison(){
		
		double result = CompareGlobals.__FAILED ;
			
		Vector<String> commonItems = new Vector<String>();
	
		int overlap;
		double matchRatio=0.0;
	
		try{
			
			Set<String> differences = new HashSet<String>( items1);
			differences.removeAll( items2 ); // what is in set 1 but not in set 2  
		
			commonItems = new Vector<String>(items1);
			commonItems.retainAll(items2) ; // what is common to set 1 and set 2
			
			overlap = commonItems.size();
			matchRatio = 1.0 - (double)( (double)(overlap*1.0)/(items1.size())) ;
			// since we calculate the distance, we have to invert the match ratio into a non-match-ratio 
			result = matchRatio;
			
			result = calculateDerivedResultsForSets( result, overlap, items1,items2) ;
			
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;
	}

	// ===============================================
	
	/**
	 * 
	 * returns a similarity NOT a distance
	 * 
	 */
	@SuppressWarnings("unchecked")
	protected double generalizedTversky(){
		double result = CompareGlobals.__FAILED ;
		
		Vector<String> commonItems = new Vector<String>();
		Vector<String> uniqueItems1 = new Vector<String>();
		Vector<String> uniqueItems2 = new Vector<String>();
		  
		int overlap, uniq1, uniq2 ;
		double coeff=0.0,a,b,s  ;
	
		try{
			
			Set<String> differences = new HashSet<String>( items1);
			differences.removeAll( items2 ); // what is in set 1 but not in set 2  
		
			commonItems = new Vector<String>(items1);
			commonItems.retainAll(items2) ; // what is common to set 1 and set 2
			
			
			uniqueItems1 = new Vector<String>( CollectionUtils.subtract(items1, items2) );
			uniqueItems2 = new Vector<String>( CollectionUtils.subtract(items2, items1) ); 
			
			 
			
			uniq1 = uniqueItems1.size() ; 
			uniq2 = uniqueItems2.size() ;
			
			overlap = commonItems.size();
			
			a = ctrlProperties.getItemSetContrastalpha();
			b = ctrlProperties.getItemSetContrastbeta();
			s = ctrlProperties.getItemSetContrastsigma();
			
			 
			 
			 
			
			coeff = (overlap*s) - (uniq1*a + uniq2*b);
			
			result = Math.max(coeff , 0.0);
			
			 
			
			result = calculateDerivedResultsForSets( result, overlap*s, items1,items2) ;
 
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;

	}

	@SuppressWarnings("unchecked")
	protected double setMatchingCRatio(){ 
		double result = CompareGlobals.__FAILED ;
		
		Vector<String> commonItems = new Vector<String>();
		Vector<String> uniqueItems1 = new Vector<String>();
		Vector<String> uniqueItems2 = new Vector<String>();
		  
		int overlap, uniq1, uniq2;
		double coeff=0.0,a,b,s ;
	
		try{
			
			Set<String> differences = new HashSet<String>( items1);
			differences.removeAll( items2 ); // what is in set 1 but not in set 2  
		
			commonItems = new Vector<String>(items1);
			commonItems.retainAll(items2) ; // what is common to set 1 and set 2
			
			
			uniqueItems1 = new Vector<String>( CollectionUtils.subtract(items1, items2) ) ;
			uniqueItems2 = new Vector<String>( CollectionUtils.subtract(items2, items1) ) ; 
			
			uniq1 = uniqueItems1.size() ; 
			uniq2 = uniqueItems2.size() ;
			
			overlap = commonItems.size();
			
			a = ctrlProperties.getItemSetContrastalpha();
			b = ctrlProperties.getItemSetContrastbeta();
			s = ctrlProperties.getItemSetContrastsigma();
			
			coeff = ((double)(overlap*1.0)*s) /( ((double)(overlap*1.0)*s) +  (((double)(uniq1*1.0)*a) + ((double)(uniq2*1.0)*b)) );
			
			result = coeff;
			
			result = calculateDerivedResultsForSets( result, overlap, items1,items2) ;
			
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;
	}
	
	@SuppressWarnings("unchecked")
	protected double setMatchingJaccard(){
		double result = CompareGlobals.__FAILED ;
		
		Vector<String> commonItems = new Vector<String>();
		Vector<String> unionItems  = new Vector<String>();
		
		  
		int overlap, union;
		double coeff=0.0  ;
	
		try{
			
			Set<String> differences = new HashSet<String>( items1);
			differences.removeAll( items2 ); // what is in set 1 but not in set 2  
		
			commonItems = new Vector<String>(items1);
			commonItems.retainAll(items2) ; // what is common to set 1 and set 2
			
			unionItems = new Vector<String>( CollectionUtils.union(items1, items2) ); 
			
			union = unionItems.size() ; 
		 	
			overlap = commonItems.size();
			 
			
			coeff = ((double)(overlap*1.0) ) /((double)(union*1.0) )  ;
			
			result = coeff;
			
			result = calculateDerivedResultsForSets( result, overlap, items1,items2) ;
			
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;
	}

	  
	protected double setMatchingSorensen(){
		double result = CompareGlobals.__FAILED ;
		
		Vector<String> commonItems = new Vector<String>();
		  
		int overlap  ;
		double coeff=0.0  ;
	
		try{
			
			Set<String> differences = new HashSet<String>( items1);
			differences.removeAll( items2 ); // what is in set 1 but not in set 2  
		
			commonItems = new Vector<String>(items1);
			commonItems.retainAll(items2) ; // what is common to set 1 and set 2
			 
			overlap = commonItems.size();
			  
			coeff = ((double)(overlap*2.0) ) /(((double)(items1.size()*1.0) ) + ((double)(items1.size()*1.0) ))  ;
			
			result = coeff;
			
			result = calculateDerivedResultsForSets( result, overlap, items1,items2) ;
			
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;
	}
	
	
	protected double mutualInformationIndex(){
		double result = CompareGlobals.__FAILED ;
		
		try{
			
			result = -1.108;
			
		}catch(Exception e){
			e.printStackTrace();
		 
		}finally{
			
		}
		return result;
	}
	// ===============================================

	protected double contextDistance(){
		double result = CompareGlobals.__FAILED ;
		
		try{
			
			result = 1.108;
			
		}catch(Exception e){
			e.printStackTrace();
		 
		}finally{
			
		}
		return result;
	}
	
	// ===============================================
	protected double stringDistance(){
		double result = CompareGlobals.__FAILED ;
		
		try{
			
			result = 1.104;
			
			
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;
	}
	
	// using adjusted edit distance
	
	
	
	
	// ===============================================

	protected double textDistance(){
		double result = CompareGlobals.__FAILED ;
		
		try{
			
			result = 1.105;
			
		}catch(Exception e){
			e.printStackTrace();
			 
		}finally{
			
		}
		return result;
	}
	
	/**
	 * 
	 * returns the maximum absolute distance for a given vector, normalized by the length of the vector
	 * 
	 * @param vector
	 * @return
	 */
	private double calculateMaximumOfPossibleDistance(Vector<Double> vector ){
		
		double result = 0.0;
		
		double vv=0.0, d=0.0, sqdsum=0.0;
		int len;
		
		try{
			
			if ((vector==null) || (vector.size()==0)){
				return result;
			}
			len = vector.size();
			
			
			for (int i=0; i<len;i++){
				
				vv = (double)vector.get(i) ;
				 		
				if (vv>0.5){
					d = (vv - 0.0);
				}else{
					d = (vv - 1.0);
				}
						
				sqdsum = sqdsum + (double)( (double)d*(double)d );
				
			} // i->
			
			result = Math.sqrt(sqdsum) ;
			
		}catch(Exception e){
			
		}
		
		
		return result ;
	}
	
	@SuppressWarnings("unused")
	private void removeEmptyItemsFromList( Vector<String> items){
		
		int i = items.size()-1;
		
		while (i>=0){
			if (items.get(i).trim().length()==0){
				items.remove(i);
			}
			i--;
		}
		
	}

	
}

